package ac.il.technion.cs236369.hw5.core.servlets.orders;

import static ac.il.technion.cs236369.hw5.core.servlets.orders.OrderItemParameters.ORDER_ITEM_INSTRUMENT_ID;
import static ac.il.technion.cs236369.hw5.core.servlets.orders.OrderItemParameters.ORDER_ITEM_SLOTS_COUNT;
import static ac.il.technion.cs236369.hw5.core.servlets.orders.OrderItemParameters.ORDER_ITEM_START_SLOT;
import static ac.il.technion.cs236369.hw5.core.servlets.orders.OrderParameters.ORDER_DATE;
import static ac.il.technion.cs236369.hw5.core.servlets.orders.OrderParameters.ORDER_ID;
import static ac.il.technion.cs236369.hw5.core.servlets.orders.OrderParameters.ORDER_PROJECT_ID;
import static ac.il.technion.cs236369.hw5.core.servlets.orders.OrderParameters.ORDER_PROJECT_NAME;
import static ac.il.technion.cs236369.hw5.core.servlets.orders.OrderParameters.ORDER_USER;
import static ac.il.technion.cs236369.hw5.core.utils.OperationResult.ERROR;
import static ac.il.technion.cs236369.hw5.core.utils.OperationResult.SUCCESS;

import java.io.IOException;
import java.sql.Date;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;
import javax.persistence.Query;
import javax.persistence.TypedQuery;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.log4j.Logger;
import org.eclipse.persistence.config.PessimisticLock;
import org.eclipse.persistence.config.QueryHints;

import ac.il.technion.cs236369.hw5.core.entities.Instrument;
import ac.il.technion.cs236369.hw5.core.entities.Order;
import ac.il.technion.cs236369.hw5.core.entities.OrderItem;
import ac.il.technion.cs236369.hw5.core.entities.Project;
import ac.il.technion.cs236369.hw5.core.entities.User;
import ac.il.technion.cs236369.hw5.core.utils.OperationResult;

import com.google.gson.Gson;

public enum OrderOperation {

	ORDER_INSERT( "order_insert") {
		private ThreadLocal<Order> order = new ThreadLocal<Order>( );
		public OrderOperation execute( HttpServletRequest request) {
			EntityManager manager = emf.createEntityManager( );
			EntityTransaction transaction = manager.getTransaction( );
			transaction.begin( );
			try {
				String instrId = request.getParameter( ORDER_ITEM_INSTRUMENT_ID.toString( ));
				String userId = request.getUserPrincipal( ).getName( );
				String projectName = request.getParameter( ORDER_PROJECT_NAME.toString( )); 
				if ( instrId == null || userId == null || projectName == null) {
					status.set( ERROR);
					logger.error( "Parameters was not sent. Operation: " + toString( ));
					return this;
				}
				Instrument instrument = manager.find(Instrument.class, Integer.valueOf( instrId));
				User user = manager.find(User.class, userId);
				Query query = manager.createQuery( "SELECT p FROM Project p WHERE p.name = :name");
				Project project = (Project)query.setParameter( "name", projectName).getSingleResult( );
				if ( instrument == null || user == null) {
					status.set( ERROR);
					logger.error( "Unable to find either user or instrument. Opeartion: " + toString( ));
					return this;
				}
				if ( user.getActivityStatus( ).before( new java.util.Date( ))) {
					status.set( ERROR);
					logger.error( "User is inactive, unable to complete order.");
					return this;
				}
				int numOfSlots = 24 * 60 / instrument.getTimeSlot( );
				int startSlot = Integer.valueOf( request.getParameter( ORDER_ITEM_START_SLOT.toString( )));
				int slotsCount = Integer.valueOf( request.getParameter( ORDER_ITEM_SLOTS_COUNT.toString( )));
				int endSlot = startSlot + slotsCount;
				if ( (numOfSlots - startSlot) < slotsCount) {
					status.set( ERROR);
					logger.error( "Incorrect trial to pick busy time slots.");
					return this;
				}
				// Check for intersection with other orders in the same date.
				TypedQuery<OrderItem> q = manager.createQuery( "SELECT i FROM OrderItem i WHERE i.order.orderDate = :date", OrderItem.class);
				q.setHint( QueryHints.PESSIMISTIC_LOCK, PessimisticLock.Lock);
				List<OrderItem> orderItems = q 
					.setParameter("date", Date.valueOf( request.getParameter( ORDER_DATE.toString( )))) 
					.getResultList( );
				Order order = new Order( );
				//ORDER_USER.saveParameters(order, request.getParameterMap( ), manager);
				order.setUser( user);
				order.setProject( project);
				ORDER_DATE.saveParameters(order, request, manager);
				for( OrderItem item : orderItems) {
					if (( item.getStartSlot( ) < startSlot &&
						 (item.getStartSlot() + item.getSlotsCount( )) > startSlot ) || 
						 ( item.getStartSlot( ) < endSlot &&
								 (item.getStartSlot() + item.getSlotsCount( )) > endSlot )) {
						status.set( ERROR);
						logger.error( "Triying to save order on busy slots. Operation: " + toString( ));
						return this;
					}
				}
				manager.persist(order);
				OrderItem orderItem = new OrderItem( );
				orderItem.setInstrument(instrument);
				orderItem.setStartSlot(startSlot);
				orderItem.setSlotsCount(slotsCount);
				orderItem.setOrder(order);
				manager.persist(orderItem);
				manager.merge(order);
				
				// Second phase.
				q = manager.createQuery( "SELECT i FROM OrderItem i WHERE i.order.orderDate = :date AND i.order.Id != :id", OrderItem.class);
				q.setHint( QueryHints.PESSIMISTIC_LOCK, PessimisticLock.Lock);
				orderItems = q 
					.setParameter("date", Date.valueOf( request.getParameter( ORDER_DATE.toString( ))))
					.setParameter("id", order.getId( ))
					.getResultList( );
				for( OrderItem item : orderItems) {
					if (( item.getStartSlot( ) < startSlot &&
						 (item.getStartSlot() + item.getSlotsCount( )) > startSlot ) || 
						 ( item.getStartSlot( ) < endSlot &&
								 (item.getStartSlot() + item.getSlotsCount( )) > endSlot )) {
						status.set( ERROR);
						logger.error( "Triying to save order on busy slots. Operation: " + toString( ));
						transaction.rollback( );
						return this;
					}
				}
				
				
				transaction.commit( );
				status.set( SUCCESS);
				this.order.set( order);
			} catch ( Exception e) {
				logger.error( e.getMessage( ));
				transaction.rollback( );
				status.set( ERROR);
			} finally {
				manager.close( );
			}
			return this;
		}
		
		public OrderOperation result( HttpServletResponse response) {
			try {
				switch( status.get( )) {
					case ERROR : {
						response.getWriter( ).println( "ERROR");
						break;
					}
					case SUCCESS: {
						OrderOutputItem item = new OrderOutputItem( );
						item.setOrderId( order.get( ).getId( ));
						item.setUserId( order.get( ).getUser( ).getLoginName( ) );
						item.setDate(  order.get( ).getOrderDate( ).toString( ));
						item.setStartSlot( ((OrderItem)order.get( ).getOrderItems( ).toArray( )[0]).getStartSlot( ));
						item.setSlotsCount( ((OrderItem)order.get( ).getOrderItems( ).toArray( )[0]).getSlotsCount( ));
						item.setTimeUnit( ((OrderItem)order.get( ).getOrderItems( ).toArray( )[0]).getInstrument( ).getTimeSlot( ));
						item.setUsagePrice( ((OrderItem)order.get( ).getOrderItems( ).toArray( )[0]).getInstrument( ).getUsagePrice( ));
						item.setProjectName( order.get( ).getProject( ).getName( ));
						Gson gson = new Gson( );
						response.getWriter( ).println( gson.toJson( item));
						break;
					}
				}
			} catch (IOException e) {
				logger.error( e.getMessage( ));			}
			return this;
		}
	},
	ORDER_UPDATE( "order_update") {
		public OrderOperation execute( HttpServletRequest request) {
			EntityManager manager = emf.createEntityManager( );
			EntityTransaction transaction = manager.getTransaction( );
			transaction.begin( );
			try {
				Integer orderId = Integer.valueOf( request.getParameter(  ORDER_ID.toString( )));
				if ( orderId == null) {
					status.set( ERROR);
					return this;
				}
				Order order = manager.find( Order.class, orderId);
				// If needed update date to the order.
				if ( request.getParameter( ORDER_DATE.toString( )) != null) {
					ORDER_DATE.saveParameters( order, request, manager);
				}
				if ( order.getOrderItems( ).size( ) == 0) {
					status.set( ERROR);
					manager.merge( order);
					transaction.commit( );
					return this;
				}
				OrderItem item = (OrderItem)order.getOrderItems( ).toArray( )[0];
				Integer instrumentId = Integer.valueOf( request.getParameter( ORDER_ITEM_INSTRUMENT_ID.toString( )));
				if ( instrumentId != null) {
					Instrument instrument =  manager.find( Instrument.class, instrumentId);
					item.setInstrument( instrument);
				}
				Integer startSlot  = Integer.valueOf( request.getParameter( ORDER_ITEM_START_SLOT.toString( )));
				Integer slotsCount = Integer.valueOf( request.getParameter( ORDER_ITEM_SLOTS_COUNT.toString( )));
				Integer numOfSlots = 24 * 60 / item.getInstrument( ).getTimeSlot( );
				if ( (numOfSlots - startSlot) < slotsCount) {
					status.set( ERROR);
					return this;
				}
				Integer endSlot = startSlot + slotsCount;
				List<OrderItem> orderItems = manager.createQuery( "SELECT i FROM OrderItem i WHERE i.order.orderDate = :date", OrderItem.class)
					.setParameter("date", order.getOrderDate( )) .getResultList( );
				for( OrderItem $ : orderItems) {
					if (( $.getStartSlot( ) <= startSlot &&
						 ($.getStartSlot() + $.getSlotsCount( )) >= startSlot ) || 
						 ( $.getStartSlot( ) <= endSlot &&
								 ($.getStartSlot() + $.getSlotsCount( )) >= endSlot )) {
						status.set( ERROR);
						return this;
					}
				}
				item.setSlotsCount( slotsCount);
				item.setStartSlot( startSlot);
				manager.merge( item);
				transaction.commit( );
				status.set( SUCCESS);
			} catch ( Exception e) {
				transaction.rollback( );
				status.set( ERROR);
				logger.error( e.getMessage( ));
			} finally {
				manager.close( );
			}
			return this;
		}
		
		public OrderOperation result( HttpServletResponse response) {
			try {
				switch( status.get( )) {
					case SUCCESS: {
						response.getWriter( ).println( "SUCCESS");
						break;
					}
					case ERROR: {
						response.getWriter( ).println( "ERROR");
						break;
					}
				}
			} catch (IOException e) {
				logger.error( e.getMessage( ));
			}
			return this;
		}
	},
	ORDER_DELETE( "order_delete") {
		// Meanwhile take care of deletion and cascade propagation here,
		// in the future use on delete cascade annotation from JPA.
		public OrderOperation execute( HttpServletRequest request) {
			EntityManager manager = emf.createEntityManager( );
			EntityTransaction transaction = manager.getTransaction( );
			transaction.begin( );
			try {
				String orderId = request.getParameter( ORDER_ID.toString( ));
				if ( orderId == null) {
					status.set( ERROR);
					logger.error( "Order id was not defined. Opeartion: " + toString( ));
					return this;
				}
				Order order = manager.find( Order.class, Integer.valueOf( orderId));
				if ( order != null)
					manager.remove( order);
				transaction.commit( );
				status.set( SUCCESS);
			} catch (Exception e ) {
				logger.error( e.getMessage( ));
				transaction.rollback( );
				status.set( ERROR);
			} finally {
				manager.close( );
			}
			
			return this;
		}
		
		public OrderOperation result( HttpServletResponse response) {
			try {
				switch( status.get( )) {
					case SUCCESS: {
						response.getWriter( ).println( "SUCCESS");
						break;
					}
					case ERROR: {
						response.getWriter( ).println( "ERROR");
						break;
					}
				}
			} catch (IOException e) {
				logger.error( e.getMessage( ));
			}
			return this;
		}
	},
	ORDER_LIST_ALL_BY_INSTRUMENT( "order_instrument_orders_list") {
		private ThreadLocal< Collection<Order>> resultsOrders = new ThreadLocal<Collection<Order>>( );
		public OrderOperation execute( HttpServletRequest request) {
			resultsOrders.set( new ArrayList<Order>( ));
			EntityManager manager = emf.createEntityManager( );
			String instrumentId = request.getParameter( ORDER_ITEM_INSTRUMENT_ID.toString( ));
			if ( instrumentId == null) {
				status.set( ERROR);
				logger.error( "Instrument id was not defined. Operation: " + toString( ));
				return this;
			}
			List< Order> orders = manager.createQuery( "SELECT o FROM Order o WHERE o.user.loginName = :ownerId", Order.class)
				.setParameter( "ownerId", request.getUserPrincipal( ).getName( )).getResultList( );
			for( Order $ : orders) {
				if ( $.getOrderItems( ).size( ) == 0)
					continue;
				OrderItem item = (OrderItem)$.getOrderItems( ).toArray( )[0];
				if ( item.getInstrument( ).getId( ).equals( Integer.valueOf( instrumentId))) {
					resultsOrders.get( ).add( $);
				}
			}
			status.set( SUCCESS);
			return this;
		}
		
		public OrderOperation result( HttpServletResponse response) {
			try {
				switch( status.get( )) {
					case SUCCESS : {
						OrdersOutputEnvelope output = new OrdersOutputEnvelope( );
						for( Order order : resultsOrders.get( )) {
							OrderOutputItem item = new OrderOutputItem( );
							item.setOrderId( order.getId( ));
							item.setDate( order.getOrderDate( ).toString( ));
							item.setUserId( order.getUser( ).getLoginName( ));
							if ( !order.getOrderItems( ).isEmpty( )) {
								OrderItem $ = (OrderItem)order.getOrderItems( ).toArray( )[0];
								item.setUsagePrice( $.getInstrument( ).getUsagePrice( ));
								item.setSlotsCount( $.getSlotsCount( ));
								item.setStartSlot( $.getStartSlot( ));
								item.setTimeUnit( $.getInstrument( ).getTimeSlot( ));
								item.setProjectName( $.getOrder( ).getProject( ).getName( ));
							}
							output.getItems( ).add( item);
						}
						Gson gson = new Gson( );
						response.getWriter( ).println( gson.toJson( output));
						break;
					}
					case ERROR : {
						response.getWriter( ).println( "ERROR");
						break;
					}
				}
			} catch (IOException e) {
				e.printStackTrace();
				logger.error( e.getMessage( ));
			}
			return this;
		}
	},
	ORDER_LIST_ALL_BY_USER( "order_user_orders_list") {
		private ThreadLocal< Collection<Order>> orders = new ThreadLocal<Collection<Order>>( );
		public OrderOperation execute( HttpServletRequest request) {
			orders.set( new ArrayList<Order>( ));
			EntityManager manager = emf.createEntityManager( );
			String userId = request.getParameter( ORDER_USER.toString( ));
			if ( userId == null) {
				status.set( ERROR);
				logger.error( "User id was not defined. Operation: " + toString( ) );
				return this;
			}
			orders.set( manager.createQuery( "SELECT o FROM Order o WHERE o.user.loginName = :userId", Order.class)
				.setParameter( "userId", userId).getResultList( ));
			status.set( SUCCESS);
			return this;
		}
		
		public OrderOperation result( HttpServletResponse response) {
			try {
				switch( status.get( )) {
					case SUCCESS : {
						OrdersOutputEnvelope output = new OrdersOutputEnvelope( );
						for( Order order : orders.get( )) {
							OrderOutputItem item = new OrderOutputItem( );
							item.setOrderId( order.getId( ));
							item.setDate( order.getOrderDate( ).toString( ));
							if ( !order.getOrderItems( ).isEmpty( )) {
								OrderItem $ = (OrderItem)order.getOrderItems( ).toArray( )[0];
								item.setItemDescription( $.getInstrument( ).getDescription( ));
								item.setItemType( $.getInstrument( ).getType( ));
								item.setTimeUnit( $.getInstrument( ).getTimeSlot( ));
								item.setUsagePrice( $.getInstrument( ).getUsagePrice( ));
								item.setSlotsCount( $.getSlotsCount( ));
								item.setStartSlot( $.getStartSlot( ));
								item.setProjectName( $.getOrder( ).getProject( ).getName( ));
							}
							output.getItems( ).add( item);
						}
						Gson gson = new Gson( );
						response.getWriter( ).println( gson.toJson( output));
						break;
					}
					case ERROR : {
						response.getWriter( ).println( "ERROR");
						break;
					}
				}
			} catch (IOException e) {
				logger.error( e.getMessage( ));
			}
			return this;
		}
	},
	ORDER_LIST_ALL_BY_PROJECT( "order_project_orders_list") {
		private ThreadLocal< Collection<Order>> orders = new ThreadLocal<Collection<Order>>( );
		public OrderOperation execute( HttpServletRequest request) {
			orders.set( new ArrayList<Order>( ));
			EntityManager manager = emf.createEntityManager( );
			String projectId = request.getParameter( ORDER_PROJECT_ID.toString( ));
			if ( projectId == null) {
				status.set( ERROR);
				logger.error( "Project id is not defined. Operation: " + toString( ));
				return this;
			}
			orders.set( manager.createQuery( "SELECT o FROM Order o WHERE o.project.id = :projectId", Order.class)
				.setParameter( "projectId", Integer.valueOf( projectId)).getResultList( ));
			status.set( SUCCESS);
			return this;
		}
		
		public OrderOperation result( HttpServletResponse response) {
			try {
				switch( status.get( )) {
					case SUCCESS : {
						OrdersOutputEnvelope output = new OrdersOutputEnvelope( );
						for( Order order : orders.get( )) {
							OrderOutputItem item = new OrderOutputItem( );
							item.setOrderId( order.getId( ));
							item.setDate( order.getOrderDate( ).toString( ));
							item.setProjectName( order.getProject( ).getName( ));
							item.setUserId( order.getUser( ).getLoginName( ));
							if ( !order.getOrderItems( ).isEmpty( )) {
								OrderItem $ = (OrderItem)order.getOrderItems( ).toArray( )[0];
								item.setItemDescription( $.getInstrument( ).getDescription( ));
								item.setItemType( $.getInstrument( ).getType( ));
								item.setTimeUnit( $.getInstrument( ).getTimeSlot( ));
								item.setUsagePrice( $.getInstrument( ).getUsagePrice( ));
								item.setSlotsCount( $.getSlotsCount( ));
								item.setStartSlot( $.getStartSlot( ));
							}
							output.getItems( ).add( item);
						}
						Gson gson = new Gson( );
						response.getWriter( ).println( gson.toJson( output));
						break;
					}
					case ERROR : {
						response.getWriter( ).println( "ERROR");
						break;
					}
				}
			} catch (IOException e) {
				e.printStackTrace();
				logger.error( e.getMessage( ));
			}
			return this;
		}
	},
	ORDER_GET_AVAILABLE_SLOTS( "order_get_slots") {
		private ThreadLocal< Boolean[]> busySlots = new ThreadLocal< Boolean[]>( );
		public OrderOperation execute( HttpServletRequest request) {
			EntityManager manager = emf.createEntityManager( );
			try  {
				String instrumentId = request.getParameter( ORDER_ITEM_INSTRUMENT_ID.toString());
				if ( instrumentId == null) {
					status.set( ERROR);
					return this;
				}
				Instrument instrument = manager.find( Instrument.class, Integer.valueOf( instrumentId));
				int timeSlot = instrument.getTimeSlot( );
				int timeSlotCnt = 24 * 60 / timeSlot;
				String date = request.getParameter( ORDER_DATE.toString( ));
				List<OrderItem> orderItems = manager.createQuery( "SELECT i FROM OrderItem i WHERE i.order.orderDate=:date", OrderItem.class)
					.setParameter("date", Date.valueOf( date))
					.getResultList( );
				Boolean[] slots = new Boolean[timeSlotCnt];
				for ( int i = 0; i < timeSlotCnt; ++ i) {
					slots[i] = false;
				}
				busySlots.set( slots);
				for ( OrderItem $ : orderItems) {
					for ( int i = $.getStartSlot( ); i < $.getSlotsCount() + $.getStartSlot( ); ++i) {
						busySlots.get( )[i] = true;
					}
				}
			} catch ( Exception e) {
				logger.error( e.getMessage( ));
			} finally {
				manager.close( );
			}
			status.set( SUCCESS);
			return this;
		}
		
		public OrderOperation result( HttpServletResponse response) {
			try {
				switch( status.get( )) {
					case ERROR : {
						response.getWriter( ).println( "ERROR");
						break;
					}
					case SUCCESS : {
						Gson gson = new Gson( );
						AvailableSlotsContainer $ = new AvailableSlotsContainer(busySlots.get( ));
						response.getWriter( ).println(gson.toJson( $));
					}
				}
			} catch (IOException e) {
				logger.error( e.getMessage( ));
			}
			return this;
		}
	},
	UNDEFINED( "undefined") {
		public OrderOperation execute( HttpServletRequest request) {
			logger.warn( "Undefined operation executed.");
			return this;
		}
		
		public OrderOperation result( HttpServletResponse response) {
			try {
				response.getWriter( ).println( "ERROR");
			} catch (IOException e) {
				logger.error( e.getMessage( ));
			}
			return this;
		}
	};
	
	private String name;
	
	private static Map<String, OrderOperation> map = new HashMap<String, OrderOperation>( );
	
	protected EntityManagerFactory emf = Persistence.createEntityManagerFactory( "jpaProvider");
	
	protected ThreadLocal<OperationResult> status = new ThreadLocal<OperationResult>( ) {
		@Override
		protected OperationResult initialValue( ) {
			return OperationResult.SUCCESS;
		}
	};
	
	protected final static Logger logger = Logger.getLogger( OrderOperation.class);
	
	static {
		for ( OrderOperation $ : OrderOperation.values( )) {
			map.put( $.name, $);
		}
	}
	
	private OrderOperation( String name) {
		this.name = name;
	}
	
	@Override
	public String toString( ) {
		return name;
	}
	
	public static OrderOperation fromString( String name) {
		OrderOperation $ = map.get( name);
		if ( $ == null)
			return UNDEFINED;
		return $;
	}
	
	abstract public OrderOperation execute( HttpServletRequest request);
	
	abstract public OrderOperation result( HttpServletResponse response);
}
